// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: linchuan

#include "coin2/exchange/okex/order_v3/parser.h"

#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "coin2/base/conversion.h"
#include "coin2/base/log.h"
#include "coin2/exchange/okex/order_v3/error_code.h"
#include "coin2/exchange/util/json_util.h"

namespace coin2::exchange::okex::order_v3 {

namespace {

using namespace coin2::exchange::okex::order_v3::error_code;

using coin::proto::AccountBalance;
using coin::proto::AccountPosition;
using coin::proto::CancelOrderResponse;
using coin::proto::FillType;
using coin::proto::ProductFillBundle;
using coin::proto::ProductOrderBundle;
using coin::proto::ProductOrderElement;
using coin::proto::SubmitOrderResponse;
using coin::proto::CurrencyTransferElement;
using coin::proto::TransferRequestProto;
using coin2::exchange::base::order::IOrderContext;
using coin2::exchange::okex::symbology::OkexCurrency;
using coin2::exchange::okex::symbology::OkexProduct;

using JsonValue = rapidjson::GenericValue<rapidjson::UTF8<>>;
using TransferType = coin::proto::TransferRequestProto::TransferType;

inline coin::proto::OrderSide GetOrderSide(const JsonValue& data) {
  using coin::proto::OrderSide;
  std::string_view direction = data["side"].GetString();
  if (direction == "buy") {
    return OrderSide::BUY_ORDER;
  } else if (direction == "sell") {
    return OrderSide::SELL_ORDER;
  } else {
    return OrderSide::UNKNOWN_ORDER_SIDE;
  }
}

inline int64_t GetProcOrderId(const JsonValue& data) {
  int64_t proc_order_id = 0;
  if (data.HasMember("client_oid")) {
    std::string_view proc_str = data["client_oid"].GetString();
    try {
      proc_order_id = stoll(proc_str.substr(2));
    } catch (std::exception& e) {
      LOG(ERROR) << e.what() << JsonToString(data);
    }
  }
  return proc_order_id;
}

inline std::string GetQuote(const char* symbol) {
  for (int n = 0; n < strlen(symbol); n++) {
    if (symbol[n] == '-') {
      return std::string(symbol + n + 1);
    }
  }
  NOTREACHED();
  return "";
}

bool IsOkexErrMessage(const JsonValue& doc) {
  if (doc.HasMember("error_code") && doc["error_code"].IsInt() && doc["error_code"].GetInt() > 0 &&
      doc.HasMember("error_message")) {
    return true;
  }
  if (doc.HasMember("error_code") && doc["error_code"].IsString() &&
      std::string_view(doc["error_code"].GetString()) != "0" && doc.HasMember("error_message")) {
    return true;
  }
  return false;
}

bool IsOrderUpdate(const rapidjson::GenericValue<rapidjson::UTF8<>>& json) {
  if (!(json.HasMember("table") && json.HasMember("data"))) {
    return false;
  }

  return (std::strncmp(json["table"].GetString(), "spot/", 5) == 0);
}

}  // namespace

std::map<std::string, TransferType> OkexParser::transfer_type_map_ {
  { "1", TransferRequestProto::DEPOSIT },
  { "2", TransferRequestProto::WITHDRAW },
  { "20", TransferRequestProto::DEPOSIT_SUBACCOUNT },
  { "21", TransferRequestProto::WITHDRAW_SUBACCOUNT },
  { "47", TransferRequestProto::DEPOSIT_SPOT },
  { "46", TransferRequestProto::WITHDRAW_SPOT },
  { "19", TransferRequestProto::DEPOSIT_FUTURES },
  { "18", TransferRequestProto::WITHDRAW_FUTURES },
  { "33", TransferRequestProto::DEPOSIT_MARGIN },
  { "34", TransferRequestProto::WITHDRAW_MARGIN },
  { "58", TransferRequestProto::DEPOSIT_PERPETUAL },
  { "57", TransferRequestProto::WITHDRAW_PERPETUAL },
};

// NOTE: when field exchange and market_type are set, which means parsed successfully
auto OkexParser::ParseBalance(const JsonValue& document) -> AccountBalance {
  /*
    [
      {
        "frozen": "0",
        "hold": "0",
        "id": "",
        "currency": "BTC",
        "balance": "0.006",
        "available": "0.006",
        "holds": "0"
      },
      {
        "frozen": "0",
        "hold": "0",
        "id": "",
        "currency": "EOS",
        "balance": "7.0002",
        "available": "7.0002",
        "holds": "0"
      }
    ]
  */
  AccountBalance acc_balance;
  if (document.IsObject() && IsOkexErrMessage(document)) {
    LOG(ERROR) << "[ParseBalance] " << JsonToString(document);
    return acc_balance;
  }
  if (!document.IsArray()) {
    LOG(ERROR) << "[ParseBalance] " << JsonToString(document);
    return acc_balance;
  }

  RawBalanceMap raw_balance_map;
  for (const auto& data : document.GetArray()) {
    if (!data.HasMember("currency") || !data.HasMember("balance") || !data.HasMember("holds") ||
        !data.HasMember("available")) {
      continue;
    }
    CurrencyBalance currency_balance;
    currency_balance.set_currency_native(data["currency"].GetString());
    currency_balance.set_total(stod(data["balance"].GetString()));
    currency_balance.set_hold(stod(data["holds"].GetString()));
    currency_balance.set_available(stod(data["available"].GetString()));
    raw_balance_map.emplace(currency_balance.currency_native(), currency_balance);
  }

  acc_balance.set_exchange("Okex");
  acc_balance.set_market_type("Spot");
  for (const auto& native_currency : symcache_->GetCurrenciesNativeUpper()) {
    auto* balance = acc_balance.add_each_balance();
    if (raw_balance_map.count(native_currency) == 0) {
      balance->set_total(0);
      balance->set_hold(0);
      balance->set_available(0);
    } else {
      balance->MergeFrom(raw_balance_map.at(native_currency));
    }
    auto* currency = symcache_->GetCurrencyByNative(native_currency);
    balance->set_currency(currency->currency());
    balance->set_currency_native(currency->native_currency());
    DLOG(INFO) << "BALANCE: " << currency->native_currency() << " " << balance->total() << " "
               << balance->available();
  }

  return acc_balance;
}

void OkexParser::ParseRestAccountBalance() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestAccountBalance] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  auto account_balance = ParseBalance(document);
  if (!account_balance.has_exchange() || !account_balance.has_market_type()) {
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  result_.set_type(MsgType::ACCOUNT_BALANCE);
  *result_.mutable_account_balance() = std::move(account_balance);
}

void OkexParser::ParseOrderInfo(const JsonValue& data, coin::proto::ProductOrderElement* order) {
  order->set_market_type(MarketType::Spot);
  order->set_exchange_type(ExchangeType::Okex);
  order->set_api_version("v3");
  order->set_exchange_order_id(data["order_id"].GetString());
  order->set_symbol(data["instrument_id"].GetString());
  order->set_price(stod(data["price"].GetString()));
  order->set_qty(stod(data["size"].GetString()));
  order->set_proc_order_id(GetProcOrderId(data));
  order->set_side(GetOrderSide(data));
  std::string_view type_str = data["type"].GetString();
  if (type_str == "limit") {
    order->set_order_type(coin::proto::OrderType::LIMIT_ORDER);
  } else if (type_str == "market") {
    order->set_order_type(coin::proto::OrderType::MARKET_ORDER);
  }
  order->set_timestamp(TimestampFromString(data["timestamp"].GetString()));
  CHECK_THROW(data.HasMember("state"));
  const int status = std::stoi(data["state"].GetString());
  // -2:Failed,-1:Canceled,0:Open ,1:Partially Filled, 2:Fully
  // Filled,3:Submitting,4:Canceling
  switch (status) {
    case -2:
    case 3:
    case 4: {
      order->set_is_live(false);
      break;
    }
    case 0:
    case 1: {
      order->set_is_live(true);
      break;
    }
    case -1: {
      order->set_cancel_confirmed(true);
      break;
    }
    case 2: {
      order->set_fully_filled(true);
      break;
    }
    default: {
      LOG(ERROR) << "Unknown order status! " << status;
      break;
    }
  }
}

void OkexParser::ParseRestOpenOrders() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestOpenOrders] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  auto product_order_bundle = result_.mutable_order_list();
  product_order_bundle->Clear();

  if (document.IsObject() && IsOkexErrMessage(document)) {
    LOG(ERROR) << "[ParseRestOrderInfoList] " << JsonToString(document);
    return;
  }
  if (!document.IsArray()) {
    LOG(ERROR) << "[ParseRestOrderInfoList] " << JsonToString(document);
    return;
  }

  bool product_order_bundle_found = false;
  for (const auto& info : document.GetArray()) {
    if (IsOkexErrMessage(info)) {
      LOG(ERROR) << "[ParseRestOrderInfoList] " << JsonToString(info);
      continue;
    }
    auto order = product_order_bundle->add_each_order();
    ParseOrderInfo(info, order);
    product_order_bundle_found = true;
  }
  if (product_order_bundle_found) {
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

void OkexParser::ParseWsOrderInfoList(const JsonDocument& document) {
  auto product_order_bundle = result_.mutable_order_list();
  product_order_bundle->Clear();

  if (!document.HasMember("data")) {
    LOG(ERROR) << "Not .data in json. " << JsonToString(document);
    return;
  }
  const auto& info_list = document["data"];
  CHECK_THROW(info_list.IsArray());

  bool product_order_bundle_found = false;
  for (const auto& info : info_list.GetArray()) {
    if (IsOkexErrMessage(info)) {
      LOG(ERROR) << "[ParseWsOrderInfoList] " << JsonToString(info);
      continue;
    }
    auto order = product_order_bundle->add_each_order();
    ParseOrderInfo(info, order);
    product_order_bundle_found = true;
  }
  if (product_order_bundle_found) {
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

void OkexParser::ParseWsFillInfoList(const JsonDocument& document) {
  auto product_fill_bundle = result_.mutable_fill_list();
  product_fill_bundle->Clear();

  if (!document.HasMember("data")) {
    return;
  }
  const auto& info_list = document["data"];
  if (info_list.Size() == 0) {
    return;
  }

  bool product_fill_bundle_found = false;
  for (const auto& info : info_list.GetArray()) {
    int status = std::stoi(info["state"].GetString());
    if (!(status == 1 || status == 2)) {
      continue;
    }
    auto fill = product_fill_bundle->add_each_fill();
    fill->set_symbol(info["instrument_id"].GetString());
    fill->set_fill_timestamp(TimestampFromString(info["last_fill_time"].GetString()));
    fill->set_exchange_order_id(info["order_id"].GetString());
    fill->set_proc_order_id(GetProcOrderId(info));
    fill->set_price(stod(info["last_fill_px"].GetString()));
    fill->set_qty(stod(info["last_fill_qty"].GetString()));
    fill->set_fill_id(info["last_fill_id"].GetString());

    // Infer fill type logic
    auto oc = ocm_->FindByProcOrderId(fill->proc_order_id());
    if (oc) {
      if (std::abs(fill->price() - oc->order_spec().price) > 1e-6) {
        fill->set_fill_type(FillType::TAKER_FILL_TYPE);
      } else if (oc->order_spec().post_only) {
        fill->set_fill_type(FillType::MAKER_FILL_TYPE);
      } else if (fill->fill_timestamp() - oc->order_created_time() > 5e9) {
        fill->set_fill_type(FillType::MAKER_FILL_TYPE);
      } else {
        fill->set_fill_type(FillType::TAKER_FILL_TYPE);
      }
    } else {
      fill->set_fill_type(FillType::TAKER_FILL_TYPE);
    }

    if (info.HasMember("fee")) {
      if (std::string fee = info["fee"].GetString(); !fee.empty()) {
        fill->set_fee(-stod(fee));
      }
    }
    if (info.HasMember("fee_currency")) {
      if (std::string fee_currency = info["fee_currency"].GetString(); !fee_currency.empty()) {
        fill->set_fee_currency(fee_currency);
      }
    }
    product_fill_bundle_found = true;
  }
  if (product_fill_bundle_found) {
    result_.set_type(MsgType::ORDER_FILL);
  }
}

void OkexParser::ParseRestFillOrders() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestFillOrders] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  auto product_fill_bundle = result_.mutable_fill_list();
  product_fill_bundle->Clear();

  if (document.IsObject() && IsOkexErrMessage(document)) {
    LOG(ERROR) << "[ParseRestFillInfoList] " << JsonToString(document);
    return;
  }
  CHECK_THROW(document.IsArray());
  if (document.Size() == 0) {
    return;
  }

  bool product_fill_bundle_found = false;
  std::string_view quote;
  for (const auto& info : document.GetArray()) {
    if (quote.empty()) {
      quote = GetQuote(info["instrument_id"].GetString());
    }
    if (std::string_view(info["currency"].GetString()) == quote) {
      // Ignore
      continue;  // TODO(xguo) set fee, fee_currency
    }
    auto fill = product_fill_bundle->add_each_fill();
    fill->set_symbol(info["instrument_id"].GetString());
    fill->set_exchange_order_id(info["order_id"].GetString());
    fill->set_price(stod(info["price"].GetString()));
    fill->set_qty(stod(info["size"].GetString()));
    fill->set_fill_id(info["trade_id"].GetString());
    std::string_view exec_type_str = info["exec_type"].GetString();
    if (exec_type_str == "M") {
      fill->set_fill_type(FillType::MAKER_FILL_TYPE);
    } else if (exec_type_str == "T") {
      fill->set_fill_type(FillType::TAKER_FILL_TYPE);
    } else {
      fill->set_fill_type(FillType::UNKNOWN_FILL_TYPE);
    }

    if (info.HasMember("fee")) {
      if (std::string fee = info["fee"].GetString(); !fee.empty()) {
        fill->set_fee(-stod(fee));
      }
    }
    if (info.HasMember("fee_currency")) {
      if (std::string fee_currency = info["fee_currency"].GetString(); !fee_currency.empty()) {
        fill->set_fee_currency(fee_currency);
      }
    }
    product_fill_bundle_found = true;
  }
  if (product_fill_bundle_found) {
    result_.set_type(MsgType::ORDER_FILL);
  }
}

void OkexParser::ParseSubmitResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::SUBMIT_RESPONSE);
  auto resp = result_.mutable_submit_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[OkexSubmitOrder] API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[OkexSubmitOrder] Parsing Error." << msg_->DebugString();
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (document.HasMember("error_code")) {
    auto error_code = GetOrderErrorCode(document["error_code"]);
    resp->set_error_code(error_code);
    resp->set_exchange_error_code(GetExchangeOrderErrorCode(document));
    resp->set_exchange_error_msg(GetExchangeOrderErrorMsg(document));
    if (error_code != coin::proto::OrderErrorCode::ORDER_NO_ERROR) {
      LOG(ERROR) << JsonToString(document);
      resp->set_success(false);
      return;
    }
  }

  CHECK_THROW(document.HasMember("result")) << http_context->res;
  if (document["result"].GetBool() == true) {
    resp->set_success(true);
    resp->set_exchange_order_id(document["order_id"].GetString());
  }
}

void OkexParser::ParseAmendResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::CANCEL_RESPONSE);
  auto cancel_resp = result_.mutable_cancel_response();
  cancel_resp->Clear();
  cancel_resp->set_symbol(order_context->order_spec().product);
  cancel_resp->set_proc_order_id(order_context->order_spec().orig_proc_order_id);
  cancel_resp->set_http_status(http_context->res.result_int());
  // cancel_resp->set_exchange_order_id(order_context->exchange_order_id());
  cancel_resp->set_timestamp(msg_->timestamp);

  result_.set_type(MsgType::SUBMIT_RESPONSE);
  auto submit_resp = result_.mutable_submit_response();
  submit_resp->Clear();
  submit_resp->set_symbol(order_context->order_spec().product);
  submit_resp->set_proc_order_id(order_context->proc_order_id());
  submit_resp->set_http_status(http_context->res.result_int());
  submit_resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    cancel_resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    cancel_resp->set_success(false);
    submit_resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    submit_resp->set_success(false);
    return;
  }

  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[OkexAmendOrder] API_LIMIT_ERROR";
    cancel_resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    cancel_resp->set_success(false);
    submit_resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    submit_resp->set_success(false);
    return;
  }

  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[OkexAmendOrder] Parsing Error." << msg_->DebugString();
    cancel_resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    cancel_resp->set_success(false);
    submit_resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    submit_resp->set_success(false);
    return;
  }

  if (document.HasMember("error_code")) {
    auto error_code = GetOrderErrorCode(document["error_code"]);
    cancel_resp->set_error_code(error_code);
    cancel_resp->set_exchange_error_code(GetExchangeOrderErrorCode(document));
    cancel_resp->set_exchange_error_msg(GetExchangeOrderErrorMsg(document));
    submit_resp->set_error_code(error_code);
    submit_resp->set_exchange_error_code(GetExchangeOrderErrorCode(document));
    submit_resp->set_exchange_error_msg(GetExchangeOrderErrorMsg(document));
    if (error_code != coin::proto::OrderErrorCode::ORDER_NO_ERROR) {
      LOG(ERROR) << "[OkexAmendOrder] error. " << JsonToString(document);
      cancel_resp->set_success(false);
      submit_resp->set_success(false);
      return;
    }
  }

  CHECK_THROW(document.HasMember("result")) << http_context->res;
  if (document["result"].GetBool() == true) {
    cancel_resp->set_success(true);
    cancel_resp->set_cancel_confirmed(true);
    submit_resp->set_success(true);
    submit_resp->set_exchange_order_id(document["order_id"].GetString());
  }
}

void OkexParser::ParseCancelResponse() {
  /*
   * It is possible to a order is submitted and accepted with order_id returned to OG.
   * But the order cannot be found later.
   *
   * normal:
   * {"client_oid":"","error_code":"0","error_message":"","order_id":"4442307910509568","result":true}
   *
   * error:
   * {"error_message":"Order does not
   * exist","code":33014,"error_code":33014,"message":"Order does not exist"}
   *
   * */
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::CANCEL_RESPONSE);
  auto resp = result_.mutable_cancel_response();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_exchange_order_id(order_context->exchange_order_id());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[OkexCancelOrder] API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[OkexCancelOrder] Parsing Error." << msg_->DebugString();
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (document.HasMember("error_code")) {
    auto error_code = GetOrderErrorCode(document["error_code"]);
    resp->set_error_code(error_code);
    resp->set_exchange_error_code(GetExchangeOrderErrorCode(document));
    resp->set_exchange_error_msg(GetExchangeOrderErrorMsg(document));
    if (error_code != coin::proto::OrderErrorCode::ORDER_NO_ERROR) {
      LOG(ERROR) << "Cancel failed! " << JsonToString(document) << ", "
                 << order_context->exchange_order_id();
      resp->set_success(false);
      return;
    }
  }

  if (document.HasMember("order_id")) {
    resp->set_exchange_order_id(std::string(document["order_id"].GetString()));
  }
  resp->set_success(true);
}

void OkexParser::ParseMultiCancelResponse() {
  /*
   * normal:
   * {
   *   "btc-usdt":[
   *     {
   *       "result":true,
   *       "client_oid":"a123",
   *       "error_message": "",
   *       "error_code": "0",
   *       "order_id": "2510832677225473"
   *     },
   *     {
   *       "result":true,
   *       "client_oid":"a1234",
   *       "error_message": "",
   *       "error_code": "0",
   *       "order_id": "2510832677225474"
   *     }
   *   ],
   *   "eth-usdt":[
   *     {
   *       "result":true,
   *       "client_oid":"a12345",
   *       "error_message": "",
   *       "error_code": "0",
   *       "order_id": "2510832677225475"
   *     },
   *     {
   *       "result":true,
   *       "client_oid":"a123456",
   *       "error_message": "",
   *       "error_code": "0",
   *       "order_id": "2510832677225476"
   *     }
   *   ]
   * }
   * */
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::CANCEL_RESPONSE);
  auto resp = result_.mutable_cancel_response();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_exchange_order_id(order_context->exchange_order_id());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[OkexCancelOrder] API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[OkexCancelOrder] Parsing Error." << msg_->DebugString();
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  for (auto itr = document.MemberBegin(); itr != document.MemberEnd(); ++itr) {
    CHECK_THROW(itr->value.IsArray()) << JsonToString(document);
    for (auto& data : itr->value.GetArray()) {
      if (!data.HasMember("order_id")) {
        continue;
      }
      if (data["order_id"].GetString() != order_context->exchange_order_id()) {
        continue;
      }
      if (data["result"].GetBool() != true) {
        resp->set_success(false);
        LOG(ERROR) << "Cancel failed! " << JsonToString(document);
      } else {
        resp->set_success(true);
        resp->set_cancel_confirmed(true);
      }
      return;
    }
  }
  resp->set_success(false);
}

void OkexParser::ParseRestTransfer() {
  /*
  [
    {
        "timestamp":"2019-03-18T07:26:50.000Z",
        "ledger_id":"3995466151",
        "created_at":"2019-03-18T07:26:50.000Z",
        "currency":"BTC",
        "amount":"0.0009985",
        "balance":"0.0049925",
        "type":"trade",
        "details":{
            "instrument_id":"BTC-USDT",
            "order_id":"2500723297813504",
            "product_id":"BTC-USDT"
        }
    }
  ]
  */

  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestTransfer] Parsing Error" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (document.IsObject() && IsOkexErrMessage(document)) {
    LOG(ERROR) << "[ParseRestTransfer] Error message" << JsonToString(document);
    return;
  }
  if (!document.IsArray()) {
    LOG(ERROR) << "[ParseRestTransfer] Invalid data format" << JsonToString(document);
    return;
  }

  auto currency_transfer_bundle = result_.mutable_currency_transfer_bundle();
  currency_transfer_bundle->Clear();
  bool currency_transfer_bundle_found = false;
  for (const auto& data : document.GetArray()) {
    if (!data.HasMember("currency") || !data.HasMember("type") || !data.HasMember("amount")) {
      LOG(ERROR) << "[ParseRestTransfer] Invalid transfer format";
      continue;
    }
    if (!symcache_->has_currency_native(data["currency"].GetString())) {
      continue;
    }
    auto* currency = symcache_->GetCurrencyByNative(data["currency"].GetString());
    auto* currency_transfer = currency_transfer_bundle->add_each_transfer();
    currency_transfer->set_currency(currency->currency());
    std::string type = data["type"].GetString();
    if (transfer_type_map_.find(type) != transfer_type_map_.end()) {
      currency_transfer->set_type(transfer_type_map_[type]);
      currency_transfer->set_status(TransferRequestProto::TRANSFER_CONFIRMED);
    }
    currency_transfer->set_transfer_id(data["ledger_id"].GetString());
    currency_transfer->set_qty(stod(data["amount"].GetString()));
    currency_transfer->set_create_timestamp(TimestampFromString(data["timestamp"].GetString()));
    currency_transfer_bundle_found = true;
  }

  if (currency_transfer_bundle_found) {
    result_.set_type(MsgType::TRANSFER);
  }
}

void OkexParser::ParseWsMessage() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseWsMessage] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  DLOG(INFO) << "OG ws msg: " << JsonToString(document);
  if (IsOrderUpdate(document)) {
    ParseWsFillInfoList(document);
    ParseWsOrderInfoList(document);
  }
}

auto OkexParser::ParseRawMessage(const RawMessage& msg) -> const ParseResult& {
  result_.Clear();
  msg_ = &msg;
  sub_topic_id_ = static_cast<OrderSubTopicId>(msg.type);

  if (msg.type == OrderSubTopicId::WS_MESSAGE) {
    ParseWsMessage();
    return result_;
  }

  switch (msg.http_context->query_type) {
    case OrderSubTopicId::SUBMIT_RESPONSE:
      ParseSubmitResponse();
      break;
    case OrderSubTopicId::AMEND_RESPONSE:
      ParseAmendResponse();
      break;
    case OrderSubTopicId::CANCEL_RESPONSE:
      ParseCancelResponse();
      break;
    case OrderSubTopicId::MULTI_CANCEL_RESPONSE:
      // pass
      break;
    case OrderSubTopicId::REST_OPEN_ORDERS:
    case OrderSubTopicId::REST_DONE_ORDERS:
      ParseRestOpenOrders();
      break;
    case OrderSubTopicId::REST_FILL_ORDERS:
      ParseRestFillOrders();
      break;
    case OrderSubTopicId::REST_ACCOUNT_BALANCE:
      ParseRestAccountBalance();
      break;
    case OrderSubTopicId::TRANSFER:
      ParseRestTransfer();
      break;
    default:
      LOG(ERROR) << "Parsing Error. " << msg_->DebugString();
      break;
  }

  return result_;
}

}  // namespace coin2::exchange::okex::order_v3
